Dart eval:动态执行 Dart 代码
介绍
dart_eval 是一种基于 Dart AOT 动态执行 Dart 代码的技术,能够实现动态化(CodePush),支持 Flutter。它包含编译器和解释器,均使用 Dart 语言编写,并支持可扩展(如扩展 Flutter 支持)。
dart_eval 由两个 Repo 构成:
- dart_eval:提供 dart 代码动态执行能力。
- flutter_eval:基于 dart_eval,扩展 Flutter 代码动态化执行能力。
dart_eval 的主要目标是实现与真实 Dart 代码的互操作性(interoperable)。真实 Dart 代码创建类可以通过一个包装器在 dart_eval 解释器中使用,而在解释器中创建的类,可以通过创建一个接口和桥接类的方式,在解释器之外使用。
dart_eval 的编译器基于 Dart Analyzer 实现,能够实现对 Dart 代码 100% 正确、语法 100% 最新的解析(尽管编译和 evaluation 还没有完全实现)。
目前,dart_eval 实现了相当多的 Dart 规范,但仍然缺少像生成器、集合和扩展方法。此外,许多标准库还没有实现。
使用方式
eval 方法的一个基本使用例子,它是在运行时执行 Dart 代码:
import 'package:dart_eval/dart_eval.dart';
void main() {
print(eval('2 + 2')); // -> 4
final program = '''
class Cat {
Cat(this.name);
final String name;
String speak() {
return name;
}
}
String main() {
final cat = Cat('Fluffy');
return cat.speak();
}
''';
print(eval(program, function: 'main')); // -> 'Fluffy'
}
编译到文件
对于大多数使用场景,建议将 Dart 代码预编译为 EVC 字节码,以避免运行时开销。(注:这仍然是运行时代码执行,只是执行了一种更加高效的代码格式)。
这允许你将多个文件编译到一个字节码快:
import 'dart:io';
import 'package:dart_eval/dart_eval.dart';
void main() {
final compiler = Compiler();
final program = compiler.compile({'my_package': {
'main.dart': '''
int main() {
var count = 0;
for (var i = 0; i < 1000; i = i + 1) {
count = count + i;
}
return count;
}
'''
}});
final bytecode = program.write();
final file = File('program.evc');
file.writeAsBytesSync(bytecode);
}
之后你可以加载并执行程序:
import 'dart:io';
import 'package:dart_eval/dart_eval.dart';
void main() {
final file = File('program.evc');
final bytecode = file
.readAsBytesSync()
.buffer
.asByteData();
final runtime = Runtime(bytecode);
runtime.setup();
print(runtime.executeLib('package:my_package/main.dart', 'main')); // -> 499500
}
使用命令行
dart_eval 命令行允许你将已有的 Dart 工程编译为 EVC 字节码,也包括运行和检查 EVC 字节码的功能。
执行下面命令,让 dart_eval 全局生效:
dart pub global activate dart_eval
编译项目
命令行支持编译标准的 Dart 工程,但是不支持 pubspec.yaml 中声明的依赖。通过下面命令执行编译:
cd my_project
dart_eval compile -o program.evc
这会在当前目录下创建一个名为 program.evc 的 EVC 文件。
编译器也支持用 JSON 编码的桥接绑定文件进行编译。要添加这些,在你的项目根目录下创建一个名为 .dart_eval
的文件夹,添加一个 bindings 子文件夹,并将 JSON 绑定文件放在那里。编译器将自动加载这些绑定,并使它们对你的项目可用。
运行项目
通过下面命令运行生成的 EVC 文件:
dart_eval run program.evc -p package:my_package/main.dart -f main
注意,run 命令不支持绑定。所以任何用绑定编译的文件,都需要在包括必要的运行时绑定的专门运行器中运行。
检查 EVC 文件
使用下面命令 dump EVC 文件的操作码:
dart_eval dump program.evc
比如上面的 Demo,对应 EVC 检查输出如下:
0: PushScope (F3:56, 'Cat.name (get)')
1: PushObjectPropertyImpl (L0[0])
2: Return (L1)
3: PushScope (F3:85, 'Cat.speak()')
4: PushObjectProperty (L0.name)
5: PushReturnValue ()
6: Return (L1)
7: PushScope (F3:30, 'Cat.()')
8: PushNull ()
9: CreateClass (F3:"Cat", super L1, vLen=1))
10: SetObjectPropertyImpl (L2[0] = L0)
11: Return (L2)
12: PushScope (F3:157, 'main()')
13: PushConstant (C0)
14: BoxString (L0)
15: PushArg (L0)
16: Call (@7)
17: PushReturnValue ()
18: PushArg (L1)
19: InvokeDynamic (L1.speak)
20: PushReturnValue ()
21: Return (L2)
返回值
在大多数情况下,dart_eval 将返回 $Value
的一个子类,如 $int
或 $String
。这些 "装箱类型 "包含关于它们是什么以及如何修改它们的信息,就像所有的 $Values
一样,你可以用 $value
属性访问它们的底层值。
然而,当处理原始值类型(int,string等)时,你可能会发现 dart_eval 直接返回底层原始值。这是由于内部的性能优化。如果你不喜欢这种不一致,你可以把函数签名的返回类型改为动态,这将迫使 dart_eval 在返回之前总是对值进行装箱。
互操作性(Interop)
Interop 是一个总的术语,我们可以在 Dart 中访问、使用和修改 dart_eval 的数据。实现这种访问是 dart_eval 的首要任务。
互操作性包含 3 个层次:
- 值互操作性
- 封装互操作性
- 桥接互操作性
值互操作性
值互操作是最基本的形式,只要 Eval 环境与一个由真正 Dart 值支持的对象一起工作就会自动发生。(因此,一个 int 和一个字符串是可以进行值互操作的,但是在 Eval 中创建的类是不可以的)。要访问 $Value
的支持对象,请使用其 $value
属性。如果该值是一个集合,如 Map 或 List,你可以使用它的 $reified
属性来解析它所包含的值。
为了支持价值互操作,一个类只需要实现 $Value
,或者集成 $Value<T>
。
封装互操作性
使用封装其可以使 Eval 环境访问在 Eval 之外创建的类上的函数和字段。它比值互操作性更强大,也比桥接互操作更简单,这使得它成为某些用例的最佳选择。要使用包装器互操作,创建一个实现 $Instance
的类。然后,覆盖 $getProperty
/ $setProperty
来定义你的字段和方法。
桥接互操作性
桥式互操作实现了最多的功能。Eval 不仅可以访问对象的字段,而且还可以进行扩展,允许你在 Eval 中创建子类并在 Eval 之外使用它们。例如,Flightstream 使用桥接互操作来创建自定义的 Flutter 小部件。
然而,它也有点难以使用,而且它不能用来包装在你不控制的代码中创建的现有对象。(为了获得最大的灵活性而牺牲简单性,你可以同时使用桥接和包装器互操作)。由于桥接互操作需要大量的模板代码,在未来我将创建一个解决方案来生成这些模板代码。
桥接互操作还要求类的定义在编译时和运行时都是可用的。(如果你只是使用 eval 方法,你就不必担心这个问题)。
example 目录下有一个以桥接互操作性为特色的例子。
插件
为了配置编译和运行时的互操作,建议创建一个 EvalPlugin,使编译器实例能够被重用。示例:
class MyAppPlugin implements EvalPlugin {
@override
String get identifier => 'package:myapp';
@override
void configureForCompile(Compiler compiler) {
compiler.defineBridgeTopLevelFunction(BridgeFunctionDeclaration(
'package:myapp/functions.dart',
'loadData',
BridgeFunctionDef(
returns: BridgeTypeAnnotation(BridgeTypeRef.type(RuntimeTypes.objectType)), params: [])
));
compiler.defineBridgeClass($CoolWidget.$declaration);
}
@override
void configureForRuntime(Runtime runtime) {
runtime.registerBridgeFunc('package:myapp/functions.dart', 'loadData',
(runtime, target, args) => $Object(loadData()));
runtime.registerBridgeFunc('package:myapp/classes.dart', 'CoolWidget.', $CoolWidget.$new);
}
}
然后你可以用 Compiler.addPlugin 和 Runtime.addPlugin 使用这个插件。
FAQ
原理是什么?
dart_eval 是一个完全基于 Dart 的字节码编译器和运行时的实现。首先,Dart Analyzer 被用来将代码解析成 AST(抽象语法树)。然后,编译器依次查看每个声明,并递归地编译成线性字节码格式。
对于运行(Evaluation),dart_eval使用Dart的优化动态分发(dispatch)。这意味着每个字节码实际上是一个实现 EvcOp 的类,我们调用其 run() 方法来执行它。字节码可以做一些事情,如在堆栈中推送和弹出值,添加数字,跳转到程序中的其他地方,以及更复杂的Dart特有的操作,如创建一个类。
性能怎么样?
初步测试表明,对于简单的代码,在 AOT 编译的 Dart 中运行的 dart_eval 比标准 AOT Dart 慢12倍左右,大约与 Ruby 这样的语言相当。
对于许多用例来说,这实际上并不重要,例如,在 Flutter 的案例中,应用程序将 99% 的性能开销花在 Flutter 框架本身。